// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk)
//
// This file is part of SharpMap.
// SharpMap is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// SharpMap is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public License
// along with SharpMap; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 

// SOURCECODE IS MODIFIED FROM ANOTHER WORK AND IS ORIGINALLY BASED ON GeoTools.NET:
/*
 *  Copyright (C) 2002 Urban Science Applications, Inc. 
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

using System;
using System.IO;
using System.Collections;
using System.Diagnostics;
using SharpMap.Geometries;

namespace SharpMap.Converters.WellKnownBinary
{
	/// <summary>
	///  Converts Well-known Binary representations to a <see cref="SharpMap.Geometries.Geometry"/> instance.
	/// </summary>
	/// <remarks>
	/// <para>The Well-known Binary Representation for <see cref="SharpMap.Geometries.Geometry"/> (WKBGeometry) provides a portable 
	/// representation of a <see cref="SharpMap.Geometries.Geometry"/> value as a contiguous stream of bytes. It permits <see cref="SharpMap.Geometries.Geometry"/> 
	/// values to be exchanged between an ODBC client and an SQL database in binary form.</para>
	/// <para>The Well-known Binary Representation for <see cref="SharpMap.Geometries.Geometry"/> is obtained by serializing a <see cref="SharpMap.Geometries.Geometry"/>
	/// instance as a sequence of numeric types drawn from the set {Unsigned Integer, Double} and
	/// then serializing each numeric type as a sequence of bytes using one of two well defined,
	/// standard, binary representations for numeric types (NDR, XDR). The specific binary encoding
	/// (NDR or XDR) used for a geometry byte stream is described by a one byte tag that precedes
	/// the serialized bytes. The only difference between the two encodings of geometry is one of
	/// byte order, the XDR encoding is Big Endian, the NDR encoding is Little Endian.</para>
	/// </remarks> 
	public class GeometryFromWKB
	{

		/// <summary>
		/// Creates a <see cref="SharpMap.Geometries.Geometry"/> from the supplied byte[] containing the Well-known Binary representation.
		/// </summary>
		/// <param name="bytes">byte[] containing the Well-known Binary representation.</param>
		/// <returns>A <see cref="SharpMap.Geometries.Geometry"/> bases on the supplied Well-known Binary representation.</returns>
		public static Geometry Parse(byte[] bytes)
		{
			// Create a memory stream using the suppiled byte array.
			using (MemoryStream ms = new MemoryStream(bytes))
			{
				// Create a new binary reader using the newly created memorystream.
				using (BinaryReader reader = new BinaryReader(ms))
				{
					// Call the main create function.
					return Parse(reader);
				}
			}
		}

		/// <summary>
		/// Creates a <see cref="SharpMap.Geometries.Geometry"/> based on the Well-known binary representation.
		/// </summary>
		/// <param name="reader">A <see cref="System.IO.BinaryReader">BinaryReader</see> used to read the Well-known binary representation.</param>
		/// <returns>A <see cref="SharpMap.Geometries.Geometry"/> based on the Well-known binary representation.</returns>
		public static Geometry Parse(BinaryReader reader)
		{
			// Get the first byte in the array.  This specifies if the WKB is in
			// XDR (big-endian) format of NDR (little-endian) format.
			byte byteOrder = reader.ReadByte();

			if (!Enum.IsDefined(typeof(WkbByteOrder), byteOrder))
			{
				throw new ArgumentException("Byte order not recognized");
			}

			// Get the type of this geometry.
			uint type = (uint)ReadUInt32(reader, (WkbByteOrder)byteOrder);

			if (!Enum.IsDefined(typeof(WKBGeometryType), type))
				throw new ArgumentException("Geometry type not recognized");

			switch((WKBGeometryType)type)
			{
				case WKBGeometryType.wkbPoint:
					return CreateWKBPoint(reader, (WkbByteOrder)byteOrder);

				case WKBGeometryType.wkbLineString:
				    return CreateWKBLineString(reader, (WkbByteOrder)byteOrder);

				case WKBGeometryType.wkbPolygon:
				    return CreateWKBPolygon(reader, (WkbByteOrder)byteOrder);

				case WKBGeometryType.wkbMultiPoint:
					return CreateWKBMultiPoint(reader, (WkbByteOrder)byteOrder);

				case WKBGeometryType.wkbMultiLineString:
				    return CreateWKBMultiLineString(reader, (WkbByteOrder)byteOrder);

				case WKBGeometryType.wkbMultiPolygon:
				    return CreateWKBMultiPolygon(reader, (WkbByteOrder)byteOrder);

				case WKBGeometryType.wkbGeometryCollection:
				    return CreateWKBGeometryCollection(reader, (WkbByteOrder)byteOrder);
					
				default:
					throw new NotSupportedException("Geometry type '" + type.ToString() + "' not supported");
			}
		}

		private static Point CreateWKBPoint(BinaryReader reader, WkbByteOrder byteOrder)
		{
			// Create and return the point.
			return new Point(ReadDouble(reader, byteOrder), ReadDouble(reader, byteOrder));
		}

		private static Point[] ReadCoordinates(BinaryReader reader, WkbByteOrder byteOrder)
		{
			// Get the number of points in this linestring.
			int numPoints = (int)ReadUInt32(reader, byteOrder);

			// Create a new array of coordinates.
			Point[] coords = new Point[numPoints];

			// Loop on the number of points in the ring.
			for (int i = 0; i < numPoints; i++)
			{
				// Add the coordinate.
				coords[i] = new Point(ReadDouble(reader, byteOrder), ReadDouble(reader, byteOrder));
			}
			return coords;
		}

		private static LineString CreateWKBLineString(BinaryReader reader, WkbByteOrder byteOrder)
		{
		    SharpMap.Geometries.LineString l = new SharpMap.Geometries.LineString();
            //l.Vertices.AddRange(ReadCoordinates(reader, byteOrder));
            Point[] arrPoint = ReadCoordinates(reader, byteOrder);
            for (int i = 0; i < arrPoint.Length; i++)
                l.Vertices.Add(arrPoint[i]);
		    
		    return l;

            
		}

		private static LinearRing CreateWKBLinearRing(BinaryReader reader, WkbByteOrder byteOrder)
		{
			SharpMap.Geometries.LinearRing l = new SharpMap.Geometries.LinearRing();
			//l.Vertices.AddRange(ReadCoordinates(reader, byteOrder));
            Point[] arrPoint = ReadCoordinates(reader, byteOrder);
            for (int i = 0; i < arrPoint.Length; i++)
                l.Vertices.Add(arrPoint[i]);

			//if polygon isn't closed, add the first point to the end (this shouldn't occur for correct WKB data)
			if (l.Vertices[0].X != l.Vertices[l.Vertices.Count - 1].X || l.Vertices[0].Y != l.Vertices[l.Vertices.Count - 1].Y)
				l.Vertices.Add(new Point(l.Vertices[0].X, l.Vertices[0].Y));
			return l;
		}

		private static Polygon CreateWKBPolygon(BinaryReader reader, WkbByteOrder byteOrder)
		{
			// Get the Number of rings in this Polygon.
			int numRings = (int)ReadUInt32(reader, byteOrder);

			Debug.Assert(numRings >= 1, "Number of rings in polygon must be 1 or more.");

			Polygon shell = new Polygon(CreateWKBLinearRing(reader, byteOrder));

			// Create a new array of linearrings for the interior rings.
			for (int i = 0; i < (numRings - 1); i++)
				shell.InteriorRings.Add(CreateWKBLinearRing(reader, byteOrder));

			// Create and return the Poylgon.
			return shell;
		}

		private static MultiPoint CreateWKBMultiPoint(BinaryReader reader, WkbByteOrder byteOrder)
		{
			// Get the number of points in this multipoint.
			int numPoints = (int)ReadUInt32(reader, byteOrder);
	
			// Create a new array for the points.
			MultiPoint points = new MultiPoint();

			// Loop on the number of points.
			for (int i = 0; i < numPoints; i++)
			{
				// Read point header
				reader.ReadByte();
				ReadUInt32(reader, byteOrder);

				// TODO: Validate type

				// Create the next point and add it to the point array.
				points.Points.Add(CreateWKBPoint(reader, byteOrder));
			}
			return points;
		}

		private static MultiLineString CreateWKBMultiLineString(BinaryReader reader, WkbByteOrder byteOrder)
		{
			// Get the number of linestrings in this multilinestring.
			int numLineStrings = (int)ReadUInt32(reader, byteOrder);

			// Create a new array for the linestrings .
			MultiLineString mline = new MultiLineString();

			// Loop on the number of linestrings.
			for (int i = 0; i < numLineStrings; i++)
			{
				// Read linestring header
				reader.ReadByte();
				ReadUInt32(reader, byteOrder);

				// Create the next linestring and add it to the array.
				mline.LineStrings.Add(CreateWKBLineString(reader, byteOrder));
			}

			// Create and return the MultiLineString.
			return mline;
		}

		private static MultiPolygon CreateWKBMultiPolygon(BinaryReader reader, WkbByteOrder byteOrder)
		{
			// Get the number of Polygons.
			int numPolygons = (int)ReadUInt32(reader, byteOrder);

			// Create a new array for the Polygons.
			MultiPolygon polygons = new MultiPolygon();

			// Loop on the number of polygons.
			for (int i = 0; i < numPolygons; i++)
			{
				// read polygon header
				reader.ReadByte();
				ReadUInt32(reader, byteOrder);

				// TODO: Validate type

				// Create the next polygon and add it to the array.
				polygons.Polygons.Add(CreateWKBPolygon(reader, byteOrder));
			}

			//Create and return the MultiPolygon.
			return polygons;
		}

		private static Geometry CreateWKBGeometryCollection(BinaryReader reader, WkbByteOrder byteOrder)
		{
			// The next byte in the array tells the number of geometries in this collection.
			int numGeometries = (int)ReadUInt32(reader, byteOrder);

			// Create a new array for the geometries.
			GeometryCollection geometries = new GeometryCollection();

			// Loop on the number of geometries.
			for (int i = 0; i < numGeometries; i++)
			{
				// Call the main create function with the next geometry.
				geometries.Collection.Add(Parse(reader));
			}

			// Create and return the next geometry.
			return geometries;
		}

		//NOT USED
		//private static int ReadInt32(BinaryReader reader, WKBByteOrder byteOrder)
		//{
		//    if (byteOrder == WKBByteOrder.Xdr)
		//    {
		//        byte[] bytes = BitConverter.GetBytes(reader.ReadInt32()); 
		//        Array.Reverse(bytes);
		//        return BitConverter.ToInt32(bytes, 0);
		//    }
		//    else
		//        return reader.ReadInt32();
		//}

		private static uint ReadUInt32(BinaryReader reader, WkbByteOrder byteOrder)
		{
			if (byteOrder == WkbByteOrder.Xdr)
			{
				byte[] bytes = BitConverter.GetBytes(reader.ReadUInt32()); 
				Array.Reverse(bytes);
				return BitConverter.ToUInt32(bytes, 0);
			}
			else
				return reader.ReadUInt32();
		}

		private static double ReadDouble(BinaryReader reader, WkbByteOrder byteOrder)
		{
			if (byteOrder == WkbByteOrder.Xdr)
			{
				byte[] bytes = BitConverter.GetBytes(reader.ReadDouble()); 
				Array.Reverse(bytes);
				return BitConverter.ToDouble(bytes, 0);
			}
			else
				return reader.ReadDouble();
		}
	}
}
